﻿namespace Jsl.FxCop
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Diagnostics.Contracts;
    using System.Drawing;
    using System.Windows.Forms;
    using Microsoft.FxCop.Sdk;

    /// <summary>
    /// A utility class to handle processing the components.
    /// </summary>
    public class ComponentProcessor : BinaryReadOnlyVisitor
    {
        private readonly ComponentDictionary components;
        private ExpressionStatement lastExpressionStatement;
        private Dictionary<Node, Expression[]> temporaryArrays;

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the <see cref="ComponentProcessor"/> class.
        /// </summary>
        /// <param name="components">The components.</param>
        private ComponentProcessor(ComponentDictionary components)
        {
            this.components = components;
        }
        #endregion Constructors

        #region Public Methods
        /// <summary>
        /// Builds a list of all the property accessors for the components.
        /// </summary>
        /// <param name="type">The type to process.</param>
        /// <returns>A list of all the property accessors for the components.</returns>
        /// <remarks>
        /// VB uses a property to access the component field. This method makes a list of all the properties and
        /// their associated fields. Fields that should be accessed through a property have the
        /// <c>AccessedThroughPropertyAttribute</c> on them.
        /// </remarks>
        public static IDictionary<PropertyNode, Field> GetComponentAccessors(TypeNode type)
        {
            if (type == null)
            {
                throw new ArgumentNullException("type");
            }

            var controlAccessors = new Dictionary<PropertyNode, Field>();
            foreach (Member member in type.Members)
            {
                var field = member as Field;
                if (field != null)
                {
                    foreach (var attribute in field.Attributes)
                    {
                        if (IsAccessedThroughPropertyAttribute(attribute))
                        {
                            var propertyNameExpression = attribute.GetPositionalArgument(0) as Literal;
                            if ((propertyNameExpression != null) && (propertyNameExpression.Type.FullName == WellKnownTypeNames.String))
                            {
                                var propertyName = (string)propertyNameExpression.Value;
                                var property = type.GetProperty(Identifier.For(propertyName));

                                if (property != null)
                                {
                                    controlAccessors.Add(property, field);
                                }
                            }
                        }
                    }
                }
            }

            return controlAccessors;
        }

        /// <summary>
        /// Processes the type building the components dictionary.
        /// </summary>
        /// <param name="components">The components.</param>
        public static void ProcessInitializeComponent(ComponentDictionary components)
        {
            var processor = new ComponentProcessor(components);
            components.Type.DoIfInitializeComponent(
                initializeComponentMethod =>
                {
                    processor.temporaryArrays = new Dictionary<Node, Expression[]>();
                    processor.Visit(initializeComponentMethod);
                    processor.temporaryArrays = null;
                });
        }

        /// <summary>
        /// Visits the assignment statement.
        /// </summary>
        /// <param name="assignment">The assignment.</param>
        public override void VisitAssignmentStatement(AssignmentStatement assignment)
        {
            // If this is a new array being constructed then add it to the temporaryArrays.
            var constructArray = assignment.Source as ConstructArray;
            if (constructArray != null)
            {
                var arraySize = constructArray.Operands[0] as Literal;
                if ((arraySize != null)
                    && (arraySize.Value is int))
                {
                    this.temporaryArrays[assignment.Target] = new Expression[(int)arraySize.Value];
                }
            }

            // Add in any array values that are set.
            var indexer = assignment.Target as Indexer;
            if (indexer != null)
            {
                Expression[] values;
                if (this.temporaryArrays.TryGetValue(indexer.Object, out values))
                {
                    var index = indexer.Operands[0] as Literal;
                    if ((index != null)
                        && (index.Value is int))
                    {
                        values[(int)index.Value] = assignment.Source;
                    }
                }
            }

            base.VisitAssignmentStatement(assignment);
        }

        /// <summary>
        /// Visits the expression statement.
        /// </summary>
        /// <param name="statement">The statement.</param>
        public override void VisitExpressionStatement(ExpressionStatement statement)
        {
            // Keep track of the last ExpressionStatement that pushed something onto the stack.
            var methodCall = statement.Expression as MethodCall;
            if ((methodCall != null)
                && (methodCall.GetMethod().ReturnType.FullName != WellKnownTypeNames.Void))
            {
                this.lastExpressionStatement = statement;
            }

            base.VisitExpressionStatement(statement);
        }

        /// <summary>
        /// Visits the method call.
        /// </summary>
        /// <param name="call">The call being visited.</param>
        public override void VisitMethodCall(MethodCall call)
        {
            Contract.Assume(call != null);

            this.ProcessCall(call);

            base.VisitMethodCall(call);
        }
        #endregion Public Methods

        #region Private Methods
        private static bool IsAccessedThroughPropertyAttribute(AttributeNode attribute)
        {
            return attribute.Type.FullName == WellKnownTypeNames.AccessedThroughPropertyAttribute;
        }

        private static bool IsEnum(TypeNode type)
        {
            return TypeNodeHelper.IsAssignableTo(type, WellKnownTypeNames.Enum);
        }

        /// <summary>
        /// Gets the component information.
        /// </summary>
        /// <param name="component">The component.</param>
        /// <returns>The component information.</returns>
        /// <remarks>
        /// The <see cref="ComponentInfo"/> is created if it doesn't already exist.
        /// </remarks>
        private ComponentInfo GetComponentInfo(Node component)
        {
            ComponentInfo componentInfo;
            if (!this.components.TryGetValue(component, out componentInfo))
            {
                componentInfo = new ComponentInfo(component);
                this.components.Add(component, componentInfo);
            }

            return componentInfo;
        }

        private void ProcessCall(MethodCall call)
        {
            Contract.Requires(call != null);

            if (call.IsCallTo(WellKnownTypeNames.ControlCollection, "Add"))
            {
                // We get the "Parent" property by parsing the Controls.Add method call.
                this.ProcessControlCollectionMethod(call);
                return;
            }

            if (call.IsCallTo(WellKnownTypeNames.TableLayoutControlCollection, "Add"))
            {
                // The TableLayoutPanel has a slightly different controls collection.
                this.ProcessTableLayoutControlCollectionMethod(call);
                return;
            }

            if (call.IsCallTo(WellKnownTypeNames.DockablePanesCollection, "AddRange"))
            {
                // The Infragistics DockAreaPane has its controls in the Panes collection.
                this.ProcessDockablePanesCollectionMethod(call);
                return;
            }

            if (call.IsCallTo(WellKnownTypeNames.ComponentResourceManager, "ApplyResources"))
            {
                // Get the values from the resources file.
                this.ProcessComponentResourceManagerMethod(call);
                return;
            }

            // Whatever this is it might be a property setting so try that.
            this.ProcessProperties(call);
        }

        /// <summary>
        /// Processes the resources.ApplyResources method.
        /// </summary>
        /// <param name="call">The method call.</param>
        private void ProcessComponentResourceManagerMethod(MethodCall call)
        {
            // Get the second parameter which is the object name to use.
            string objectName;
            if (!call.Operands[1].TryGetLiteral(out objectName))
            {
                return;
            }

            // The parameter should be a field or a local variable. Ignore if anything more complex.
            Node parameterTarget = call.Operands[0].GetSimpleExpression();
            if (parameterTarget == null)
            {
                return;
            }

            // Process all the "properties" for this component.
            var resources = this.components.Resources;
            if (resources != null)
            {
                var componentInfo = this.GetComponentInfo(parameterTarget);
                var startsWith = objectName + ".";
                foreach (DictionaryEntry entry in resources)
                {
                    var key = (string)entry.Key;
                    if (key.StartsWith(startsWith, StringComparison.Ordinal))
                    {
                        var propertyName = key.Substring(startsWith.Length);
                        componentInfo[propertyName] = entry.Value;
                    }
                }
            }
        }

        private void ProcessControlCollectionMethod(MethodCall call)
        {
            Contract.Assume(call.Operands[0] != null);

            // The parameter should be a field or a local variable. Ignore if anything more complex.
            var parameterTarget = call.Operands[0].GetSimpleExpression();
            if (parameterTarget == null)
            {
                return;
            }

            // The target should be a method call to get_Controls.
            var getControlsCall = call.Callee.GetMethodCall();
            if ((getControlsCall == null)
                || !getControlsCall.IsCallTo(WellKnownTypeNames.Control, "get_Controls"))
            {
                return;
            }

            // This should be the field/local/this the control is being added to. If it can't be resolved to a simple
            // expression then use the more complex expression.
            var targetObject = ((MemberBinding)getControlsCall.Callee).TargetObject;
            var simpleTargetObject = targetObject.GetSimpleExpression();
            this.GetComponentInfo(parameterTarget)["Parent"] = simpleTargetObject ?? targetObject;
        }

        private void ProcessProperties(MethodCall call)
        {
            // All setter methods have a single parameter. Skip if obviously not a setter method.
            if (call.Operands.Count != 1)
            {
                return;
            }

            // All setter methods start with "set_". Skip if obviously not a setter method.
            var setterMethod = call.GetMethod();
            if ((setterMethod == null)
                || !setterMethod.Name.Name.StartsWith("set_", StringComparison.Ordinal))
            {
                return;
            }

            // Get the property name from the method name.
            var propertyName = setterMethod.Name.Name.Substring(4);

            var memberBinding = call.Callee as MemberBinding;
            if (memberBinding == null)
            {
                return;
            }

            var targetObject = memberBinding.TargetObject.GetSimpleExpression();
            if (targetObject == null)
            {
                return;
            }

            // Process the property value based on its type.
            var propertyType = setterMethod.Parameters[0].Type;
            object propertyValue;
            if (IsEnum(propertyType))
            {
                int intValue;
                if (!call.Operands[0].TryGetLiteral(out intValue))
                {
                    return;
                }

                propertyValue = intValue;
            }
            else if (propertyType == FrameworkTypes.Int32)
            {
                int value;
                if (!call.Operands[0].TryGetLiteral(out value))
                {
                    return;
                }

                propertyValue = value;
            }
            else if (propertyType == FrameworkTypes.Boolean)
            {
                // Booleans are exposed as Int32's.
                int value;
                if (!call.Operands[0].TryGetLiteral(out value))
                {
                    return;
                }

                propertyValue = value != 0;
            }
            else if (propertyType == FrameworkTypes.String)
            {
                string value;
                if (!call.Operands[0].TryGetLiteral(out value))
                {
                    return;
                }

                propertyValue = value;
            }
            else if (propertyType.FullName == WellKnownTypeNames.Point)
            {
                Point value;
                if (!call.Operands[0].TryGetPoint(out value))
                {
                    return;
                }

                propertyValue = value;
            }
            else if (propertyType.FullName == WellKnownTypeNames.Size)
            {
                Size value;
                if (!call.Operands[0].TryGetSize(out value))
                {
                    return;
                }

                propertyValue = value;
            }
            else if (propertyType.FullName == WellKnownTypeNames.SizeF)
            {
                SizeF value;
                if (!call.Operands[0].TryGetSizeF(out value))
                {
                    return;
                }

                propertyValue = value;
            }
            else if (propertyType.FullName == WellKnownTypeNames.Font)
            {
                Font font;
                if (!call.TryGetFont(out font))
                {
                    return;
                }

                propertyValue = font;
            }
            else
            {
                // If the property takes something that is either this, a field or a local then process it.
                Node value = call.Operands[0].GetSimpleExpression();
                if (value == null)
                {
                    return;
                }

                propertyValue = value;
            }

            // Add the property value.
            var componentInfo = this.GetComponentInfo(targetObject);
            componentInfo[propertyName] = propertyValue;
            componentInfo.SetSourceContext(call.SourceContext);
        }

        private void ProcessDockablePanesCollectionMethod(MethodCall call)
        {
            // Make sure we got the target for the Panes collection.
            var getPanesMethodCall = this.lastExpressionStatement.Expression as MethodCall;
            if ((getPanesMethodCall == null)
                || !getPanesMethodCall.IsCallTo(WellKnownTypeNames.DockableGroupPane, "get_Panes"))
            {
                return;
            }

            // Get the DockableGroupPane we added panes to.
            var groupPane = getPanesMethodCall.GetTargetObject().GetSimpleExpression();
            if (groupPane == null)
            {
                return;
            }

            // The parameter should be a field or a local variable. Ignore if anything more complex.
            var parameterTarget = call.Operands[0].GetSimpleExpression();
            if (parameterTarget == null)
            {
                return;
            }

            Expression[] values;
            if (this.temporaryArrays.TryGetValue(parameterTarget, out values))
            {
                // Set the Panes property value to the array we collected earlier.
                this.GetComponentInfo(groupPane)["Panes"] = values;

                // Set the Parent property of each of the panes.
                foreach (var value in values)
                {
                    // Don't set the Parent value if the target isn't a field or local.
                    var simpleValue = value.GetSimpleExpression();
                    if (simpleValue != null)
                    {
                        this.GetComponentInfo(simpleValue)["Parent"] = groupPane;
                    }
                }
            }
        }

        private void ProcessTableLayoutControlCollectionMethod(MethodCall call)
        {
            // The parameter should be a field or a local variable. Ignore if anything more complex.
            var parameterTarget = call.Operands[0].GetSimpleExpression();
            if (parameterTarget == null)
            {
                return;
            }

            // If we can get the column and row data then add it to the ComponentInfo.
            if (call.Operands.Count == 3)
            {
                int column;
                int row;
                if (call.Operands[1].TryGetLiteral(out column)
                    && call.Operands[2].TryGetLiteral(out row))
                {
                    this.GetComponentInfo(parameterTarget)["CellPosition"] = new TableLayoutPanelCellPosition(column, row);
                }
            }

            // The target should be a method call to get_Controls.
            var getControlsCall = call.Callee.GetMethodCall();
            if ((getControlsCall == null)
                || !getControlsCall.IsCallTo(WellKnownTypeNames.TableLayoutPanel, "get_Controls"))
            {
                return;
            }

            // This should be the field/local/this the control is being added to.
            var targetObject = ((MemberBinding)getControlsCall.Callee).TargetObject.GetSimpleExpression();
            if (targetObject != null)
            {
                this.GetComponentInfo(parameterTarget)["Parent"] = targetObject;
            }
        }
        #endregion Private Methods
    }
}